package aaa

// 🦜 : ^^^ the newer webflux... this seems to alter the default static file routing behavior...

import aaa.generated_pb.GreeterGrpcKt.GreeterCoroutineStub
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.ObjectNode
import com.networknt.schema.JsonSchemaFactory
import com.networknt.schema.SpecVersion
import io.grpc.ManagedChannelBuilder
import io.grpc.StatusException
import io.swagger.v3.oas.annotations.Hidden
import io.swagger.v3.oas.annotations.OpenAPIDefinition
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.info.Info
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.ExampleObject
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.tags.Tag
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.context.event.EventListener
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.data.jpa.domain.Specification
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
import org.springframework.data.jpa.repository.Query
import org.springframework.http.*
import org.springframework.http.client.reactive.ReactorClientHttpConnector
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import org.springframework.stereotype.Component
import org.springframework.web.bind.annotation.*
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.servlet.config.annotation.*
import reactor.netty.http.client.HttpClient
import reactor.netty.transport.ProxyProvider
import java.io.BufferedReader
import java.io.File
import java.io.InputStream
import java.io.InputStreamReader
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.time.Duration
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.stream.Collectors
import aaa.generated_pb.GetLoginHistoryRpl as pb_GetLoginHistoryRpl
import aaa.generated_pb.GetUsersRpl as pb_GetUsersRpl
import aaa.generated_pb.empty as pb_empty
import aaa.generated_pb.getLoginHistoryReq as pb_getLoginHistoryReq
import aaa.generated_pb.helloRequest as pb_helloRequest
import aaa.generated_pb.oneoffPack as pb_oneoffPack

interface BussinessRecordRepo: JpaRepository<Dt.BussinessRecord, Long>, JpaSpecificationExecutor<Dt.BussinessRecord>{}
@Tag(name = "Bussiness Record", description = "🦜: Bussiness records")
@RestController
@RequestMapping("/bs")
internal class BsController(): ControllerBase(){
    @Autowired lateinit var repo: BussinessRecordRepo
    val rules = mutableListOf<Dt.BussinessAuditRule>()

    @EventListener
    fun init(e: ApplicationReadyEvent?) {
        // 1. put in the mocked BussinessRecords

        // class BussinessRecord(@Id var id: Long = 0, var from: String = "", var to: String = "", var type: String = "", var cmd: String = "", var time: Date? = Date(0), var auditResult: String = "", var auditReason: String = "")

        var c = Calendar.getInstance() // now
        repo.save(Dt.BussinessRecord(1, "u[a-zA-Z]+", "u[1-9]", "TYPE1", "检查在地区AZ264-AU部署的S1服务器集群", c.time, "", ""))
        c.add(Calendar.DATE, 1)
        repo.save(Dt.BussinessRecord(2, "u2", "u3", "TYPE2", "整理并给领导发送周报", c.time, "", ""))
        c.add(Calendar.DATE, 1)
        repo.save(Dt.BussinessRecord(3, "u3", "u4", "TYPE3", "定一份冰激凌外买", c.time, "", ""))

        // 2. put in some mocked rules
        // class BussinessAuditRule(@Id var id: Long = 0, var name: String = "", var description: String = "", var from: String = "", var to: String = "",
        //                          var startTime: Date? = Date(0), var endTime: Date? = Date(0), var repeat: String = "")
        c = Calendar.getInstance()
        var c1 = Calendar.getInstance()
        c.add(Calendar.HOUR, -1)
        c1.add(Calendar.HOUR, 1)
        rules += Dt.BussinessAuditRule(1, "白名单告警", "文件接收人不在文电系统设置的白名单中；IP语音接收方不在白名单中； 导入导出申请者不在白名单中", "u1", "u2", "领导",c.time, c1.time, "")
        rules += Dt.BussinessAuditRule(2, "非工作日（闲时）告警", "文件发送时间不在文电系统设置的工作时间范围内；IP语音拨打时不在设置的时间范围内；导入导出操作时间不在设置范围内；", "u2", "", "",c.time, c1.time, "weekly")
        c.add(Calendar.DATE, 1)
        c1.add(Calendar.DATE, 1)
        rules += Dt.BussinessAuditRule(3, "导入导出系统重复告警", "同一个申请单存在重复操作；", "u1", "u3", "", c.time, c1.time, "")
        rules += Dt.BussinessAuditRule(4, "区块链告警", "线下删除或者篡改同步过来的业务数据/区块链上链信息操作。", "u1", "u3", "", c.time, c1.time, "")
    }

    @GetMapping("/getRules") @Operation(summary = "Get bussiness records audit rules")
    fun getRules() : W<List<Dt.BussinessAuditRule>> = W(0, rules)

    fun audit0(){
        log.info("🐢 : audit0 called")
        // 1. get all the records
        val l = repo.findAll()
        log.info("🐢 : found ${l.size} records to audit, number of rules = ${this.rules.size}")
        // 2. for each record, check if it's in the rule list
        for (r in l){
            r.auditResult = "OK"
            for (rule in this.rules){
                log.info("🐢 : checking rule: ${rule.id}, ${rule.keyWord} -> ${r.cmd}")
                var s = "违反了 规则${rule.id}: ${rule.name}"

                // 🦜 : for now, just checking the name should be fine
                if (rule.keyWord.isNotEmpty() && r.cmd.contains(rule.keyWord)){
                    r.auditResult = "NG"
                    r.auditReason = s
                    break
                }
            }
        }
        // 4. save the records
        repo.saveAll(l)
    }

    @PostMapping("/audit") @Operation(summary = "Audit the bussiness records. Require a rule list")
    fun audit(@RequestBody rules0: List<Dt.BussinessAuditRule>): W<String>{
        log.info("🐢 : audit called with rules:")
        for (r in rules0){
            log.info("🐢 : rule: ${r.id}, ${r.name}, ${r.keyWord}")
        }
        // 1. save the rules
        this.rules.clear()
        this.rules.addAll(rules0)
        // 2. audit the records
        this.audit0()
        return W(0, "OK")
    }

    @PostMapping("/postRecords") @Operation(summary = "Upload the bussiness records")
    fun addRecords(@RequestBody rules: List<Dt.BussinessRecord>): W<String>{
        // 0. clear the db
        repo.deleteAll()
        // 1. save the records
        repo.saveAll(rules)
        return W(0, "OK")
    }

    @GetMapping("/getRecords") @Operation(summary = "Get bussiness records")
    fun getRecords(
        @RequestParam(defaultValue = "1") @Parameter(
            description = "page number（1-based）",
            examples = [ExampleObject(name = "1", value = "1", description = "The first page")]
        ) pageNum: Int,
        @RequestParam(defaultValue = "2") @Parameter(
            description = "how many records per page",
            examples = [ExampleObject(name = "2", value = "2", description = "2 records per page")]
        ) pageSize: Int,
        @RequestParam(required = false) @Parameter(
            description = "the `from` User Name",
            examples = [ExampleObject(name = "me", value = "me", description = "Search the result for me")]
        ) from: String?,
        @RequestParam(required = false) @Parameter(
            description = "After which time",
            examples = [ExampleObject(name = "2021-01-01 00:00:00", value = "2021-01-01 00:00:00", description = "After this time")]
        ) sinceTime: String?,
        @RequestParam(required = false) @Parameter(
            description = "Before which time",
            examples = [ExampleObject(name = "2021-01-01 00:00:00", value = "2025-01-01 00:00:00", description = "Before this time")]
        ) untilTime: String?,
        @RequestParam(required = false) @Parameter(
            description = "Sort by which column", required = false,
            examples = [ExampleObject(name = "time", value = "time", description = "Sort by time"),
                        ExampleObject(name = "", value = "", description = "Sort by id")]
        ) sortBy: String?,
        @RequestParam(required = false) @Parameter(description = "Ascending", required = false) asc: Boolean?
    ): W<Dt.Page<Dt.BussinessRecord?>>{
        log.info(
            "{}🐸 : get (record) called with pageNum={}, pageSize={}, from={}, sinceTime={}, untilTime={}, sortBy={}, asc={} {}",
            S.CYAN, pageNum, pageSize, from, sinceTime, untilTime, sortBy, asc, S.NOR
        )

        // 1. prepare pageable
        val sortBys: Set<String> = setOf("time","id")
        val pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "id")
        val page: Page<Dt.BussinessRecord?>

        // 2. prepare spec
        var spec = Specification.where<Dt.BussinessRecord?>(null)
        if (from != null && from.isNotEmpty()) {
            spec = spec.and(Dt.BussinessRecord_.fromContains(from))
        }


        if (!sinceTime.isNullOrEmpty()) {
            val sinceTimeLocalDateTime = LocalDateTime.parse(sinceTime, fmt)
            val sinceTimeDate = new2Old(sinceTimeLocalDateTime)
            spec = spec.and(Dt.BussinessRecord_.timeAfter(sinceTimeDate))
        }

        if (!untilTime.isNullOrEmpty()) {
            val untilTimeLocalDateTime = LocalDateTime.parse(untilTime, fmt)
            val untilTimeDate = new2Old(untilTimeLocalDateTime)
            spec = spec.and(Dt.BussinessRecord_.timeBefore(untilTimeDate))
        }
        // 3. find and return
        page = repo.findAll(spec, pageable)
        return W(0, Dt.Page(page.content, page.totalElements, page.totalPages))
    }

}

enum class AuditType {MOCK, OLLAMA, GEMINI, QWEN25}
class AiAuditOven(val uid2user: Map<Int, Dt.UserInfo2>,
                  var url : String = "localhost:11434"){

    val ep : String get() = "http://" + this.url + "/api/generate"
    val running = AtomicBoolean(false)
    val N = AtomicLong(0L) //total number of records
    val D = AtomicLong(-1L)           //processed number of records
    var thread: Thread? = null
    var gk = ""             //gemini key

    companion object {

        // post
        fun post(ep: String, body: String, px: Boolean = false,
                 qp: Map<String, String>? = null): ResponseEntity<String> {

            // --------------------------------------------------
            var client = WebClient.create()
            if (px) {
                println("🦜 : posting with proxy")
                val httpClient =
                    HttpClient.create()
                    .proxy { proxy: ProxyProvider.TypeSpec ->
                        proxy.type(ProxyProvider.Proxy.HTTP)
                            .host("localhost")
                            .port(7890)
                    }
                val connector = ReactorClientHttpConnector(httpClient)
                client = WebClient.builder()
                    .clientConnector(connector).build()
            }

            var r0: WebClient.RequestBodySpec
            if (qp != null) {
                r0 = client.post()
                r0.uri(ep,
                       {
                           var x = it
                           for ((k, v) in qp)
                               x = x.queryParam(k, v)
                           x.build()
                       }
                )

                // for ((k, v) in qp)
                //     r0.queryParam(k, v)
            }else{
                r0 = client.post().uri(ep)
            }

            val r = r0.contentType(MediaType.APPLICATION_JSON)
                .bodyValue(body)
                .retrieve()
                .toEntity(String::class.java)
                .block()

            return r!!
        }
    }

    fun doIt(repo: OneoffProcessInfo2Repo,
             type: AuditType = AuditType.MOCK,
             Nb: Int = 5){
        this.running.set(true)
        /*
         1. Count all the records whose `spsReason: String` field is empty.
         2. Page through all these records and process each page
         */
        N.set(repo.countBySpsReason(""))
        D.set(0L)
        println("\t⚙️ total number of records to process: $N")

        // 2. page through
        // 2.1 prepare the spec
        while(running.get()) {
            // 2.2 prepare the page request
            val pg: Pageable = PageRequest.of(0, Nb, Sort.by("id").ascending())
            val spec = Specification.where<Dt.OneoffProcessInfo2?>(null).and(Dt.OneoffProcessInfo2_.spsReasonIsEmpty())
            /* 🦜 : Wait*3, I just realize something... Because we're updating
               the database on the fly, it seems like we only need to fetch the
               page 0 of these each time...So yeah, this is pretty much a do..until loop...
             */

            // 2.3 get the search result
            val l = repo.findAll(spec, pg)
            if (l.isEmpty()) break

            // 2.4 process
            val l0 = when(type) {
                AuditType.MOCK -> process(l)
                AuditType.OLLAMA -> processWithOllama(l)
                AuditType.GEMINI -> processWithGemini(l)
                AuditType.QWEN25 -> processWithQwen25(l)
            }
            // 2.5 save
            repo.saveAll(l0)
            // 2.6 update the D
            D.addAndGet(l0.size.toLong())
            println("\t⚙️ processed ${l0.size} records, now [${D.get()}/${N.get()}]")
        }
        running.set(false)
    }


    fun process(l: Iterable<Dt.OneoffProcessInfo2>): List<Dt.OneoffProcessInfo2>{
        // println("🦜 : Processing ")
        // sleep for a while
        TimeUnit.SECONDS.sleep(2)
        // 🦜 : let's mock them
        val o = l.toList()
        for (i in o){
            i.spsReason = "mocked"
            i.spsScore = 0.7f
        }

        return o
    }

    // --------------------------------------------------

    fun processWithQwen25(l0: Iterable<Dt.OneoffProcessInfo2>): List<Dt.OneoffProcessInfo2>{
        var l = l0.toList()
        val a = Dt.OneoffProcessInfo2.toAuditJsonArray(l,uid2user)
        // 1. prepare the q
        val q = prepareMlPromptZh(a)

        // 2. prepre the request json
        val json = ObjectMapper()

        val o = json.createObjectNode()
        o.put("model", "deepseek-r1:7b")
        // o.put("model", "qwen2.5-coder:7b") //🦜 : It seems like the 3b version is a little too weak...
        o.put("prompt", q)
        o.put("stream", false)

        // 2. post it until the check of json is passed
        // while (true){
        while (this.running.get()){
            val r = post(ep, o.toString())
            // 3. give the input node and check
            try{
                checkStatusAndBody(r)
                val o0 = json.readTree(r.body!!) //throws JsonParseException
                var o1 = o0.get("response").asText()

                o1 = o1.trim()
                // if within o1, there's a <think> tag, then we need to remove the tag and the content within.
                if (o1.contains("<think>")){
                    val i = o1.indexOf("<think>")
                    val j = o1.indexOf("</think>")
                    o1 = o1.substring(0,i) + o1.substring(j+8)
                }

                o1 = o1.replace("```json","")
                o1 = o1.replace("```", "")

                val o2 = readAndCheckOutput(a,o1)

                // 4. update the records
                for (i in 0 until a.size()){
                    val n = o2.get(i) as ObjectNode
                    l[i].spsScore = n.get("suspicious").asDouble().toFloat()
                    l[i].spsReason = n.get("reason").asText()
                }
                return l
            }catch(e: Exception){
                // 🦜 : absolutely normal, ai not working correctly, let's try again
                println("\t 🦜 : error processing the output, retrying: ${S.RED}$e${S.NOR}")
                continue
            }
        }
        println("\t Processing aborted, returning mock result")
        return process(l0)
    }

    fun processWithGemini(l0: Iterable<Dt.OneoffProcessInfo2>): List<Dt.OneoffProcessInfo2>{
        if (gk.isEmpty()) throw RuntimeException("🦜 : gemini key is empty")

        // 0. prepare the input json array
        var l = l0.toList()
        val a = Dt.OneoffProcessInfo2.toAuditJsonArray(l,uid2user)
        // 1. prepare the q
        val q = prepareMlPrompt(a)

        // 2. prepre the request json
        val json = ObjectMapper()

        val o0 = json.readTree("""
{"contents" : [{"parts": [{"text": "aaa"}]}]}
                               """)
        val p1 = o0.path("contents").path(0).path("parts").path(0) as ObjectNode
        p1.put("text",q)
        println("🦜 : prepared Gemini prompt: ${S.GREEN}$o0{S.NOR}")

        while (this.running.get()){
            val gep = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent"

            val r = post(gep, o0.toString(),true,
                         mapOf("key" to gk))
            println("🦜 : Got: response from gemini: ${S.GREEN}${r.body}${S.NOR}")

            try{
                checkStatusAndBody(r)
                // --------------------------------------------------
                val o3 = json.readTree(r.body!!) //throws JsonParseException

                // println("🦜 : successfully parsed the gemini body as json")
                // var m1 = o3.get("candidates")
                // println("level 1 : $m1")
                // m1 = m1.get(0)
                // println("level 2 : $m1")
                // var o1 = m1.get("content").get("parts").get(0).get("text").asText()
                // -------------------------------------------------- ^ 🦜 : for debug purose
                var o1 = o3.get("candidates").get(0).get("content").get("parts").get(0).get("text").asText()

                o1 = o1.trim()
                o1 = o1.replace("```json","")
                o1 = o1.replace("```", "")

                val o2 = readAndCheckOutput(a,o1)

                // 4. update the records
                for (i in 0 until a.size()){
                    val n = o2.get(i) as ObjectNode
                    l[i].spsScore = n.get("suspicious").asDouble().toFloat()
                    l[i].spsReason = n.get("reason").asText()
                }
                return l

            }catch(e: Exception){
                // 🦜 : absolutely normal, ai not working correctly, let's try again
                println("\t 🦜 : error processing the output, retrying: ${S.RED}$e${S.NOR}")
                continue
            }

        }

        println("\t Processing aborted, returning mock result")
        return process(l0)
    }
/*
 fun prepareMlPrompt(s: ArrayNode): String {
 val p0 = """
 Hi, can you help me audit the following list of commands?
 """
 val p1 = """
 So for each entry in the input array, I hope you can produce a result object
 that rates the input and says why.

 For example, for the following input:

 [{"cmd": "ls", "args": ["/home/me/音乐"], "user" : "me"},
 {"cmd": "cat", "args": ["/etc/passwd"], "user" : "root"}]

 Please reply me with:

 [{"suspicious" : 0.2,"reason" : "Just listing the contents of Music."},
 {"suspicious" : 0.8,"reason" : "Inspect sensitive system password file."}]
 """
 return p0 + s.toString() + p1 +
 "So just return a json array of " +
 s.size().toString() +
 " objects, each with a \"suspicious\" and \"reason\" fields."
 }
 */
    fun prepareMlPrompt(s: ArrayNode): String {
        val prompt = """
Hi, can you help me audit the following list of commands?

For each entry in the input array, please produce a result object that rates the input and provides a reason.

Example input:
[{"cmd": "ls", "args": ["/home/me/音乐"], "user" : "me"},
{"cmd": "cat", "args": ["/etc/passwd"], "user" : "root"}]

Example output:
[{"suspicious" : 0.2, "reason" : "Just listing the contents of Music."},
{"suspicious" : 0.8, "reason" : "Inspecting sensitive system password file."}]

Input:
${s.toString()}

Please return a JSON array of ${s.size()} objects, each with "suspicious" and "reason" fields.
            """
        return prompt.trimIndent()
    }

    fun prepareMlPromptZh(s: ArrayNode): String {
        val prompt = """
哈喽，你能帮我审计一下以下这些我们系统上的命令吗:

比如说，如果输入是:

[{"cmd": "ls", "args": ["/home/me/音乐"], "user" : "me"},
{"cmd": "cat", "args": ["/etc/passwd"], "user" : "root"}]

请回复我：

[{"suspicious" : 0.2, "reason" : "只是查看音乐文件夹。"},
{"suspicious" : 0.8, "reason" : "查看敏感的系统密码文件。"}]

输入:
${s.toString()}

请返回一个长度为 ${s.size()} 的 JSON 数组，里面每个对象都包含 "suspicious" 和 "reason" 字段。
            """
        return prompt.trimIndent()
}

    fun checkStatusAndBody(r: ResponseEntity<String>){

                if (r.statusCode != HttpStatus.OK){
                    throw RuntimeException("🦜 : the response status is not OK:" + r.body)
                }
                if (r.body == null){
                    throw RuntimeException("🦜 : the response body is null")
               }
    }

    // --------------------------------------------------
    // ollama related
    fun processWithOllama(l0: Iterable<Dt.OneoffProcessInfo2>): List<Dt.OneoffProcessInfo2>{
        // 🦜 : here we assume a pre-configured model called `aaa` that can do a
        // json-in and json-out

        // 0. build the input json, array
        var l = l0.toList()
        val a = Dt.OneoffProcessInfo2.toAuditJsonArray(l,uid2user)
        // 1. make the input object
        val json = ObjectMapper()
        val o = json.createObjectNode()
        o.put("model", "aaa")
        o.put("prompt", json.writeValueAsString(a))
        o.put("stream", false)

        // 2. post it until the check of json is passed
        while (this.running.get()){
            val r = post(ep, o.toString())
            // 3. give the input node and check
            try{
                checkStatusAndBody(r)
                val o0 = json.readTree(r.body!!) //throws JsonParseException
                val o1 = o0.get("response")
                if (!o1.isTextual)
                    throw RuntimeException("the response output is not a string for json array: " + o1)
                val o2 = readAndCheckOutput(a,o1.asText())

                // 4. update the records
                for (i in 0 until a.size()){
                    val n = o2.get(i) as ObjectNode
                    l[i].spsScore = n.get("suspicious").asDouble().toFloat()
                    l[i].spsReason = n.get("reason").asText()
                }
                return l
            }catch(e: Exception){
                // 🦜 : absolutely normal, ai not working correctly, let's try again
                println("\t 🦜 : error processing the output, retrying: ${S.RED}$e${S.NOR}")
                continue
            }
        }

        println("\t Processing aborted, returning mock result")
        return process(l0)
    }

    // --------------------------------------------------

    fun readAndCheckOutput(i: ArrayNode, o: String): ArrayNode{
          // Check the following
          // 1. the string o is a valid json array
          // 2. the sizes of i and o are the same
          // 3. each element in o has fields: "suspicious"  : <float>, "reason" : <string>

          val json = ObjectMapper()
          // 1.
          val oJson: ArrayNode
          println("🦜 : parsing output json string")
          // 1.1 trim o until the first '[' (beginning of the array)
          val o2 = json.readTree(o.substring(o.indexOf('[')))
          if (!o2.isArray())
              throw RuntimeException("the response output is not a json array: " + o2)

          oJson = o2 as ArrayNode

          // 2.
          if (i.size() != oJson.size()){
              throw RuntimeException("the sizes of input and output are different, output: "+ oJson)
          }

          // 3. 
          val f = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4);
          val jsk = f.getSchema("""
  {
    "type": "array",
    "items": {
      "type": "object",
      "properties" : {
        "suspicious" : {"type" : "number"},
        "reason" : {"type" : "string"}
      },
      "required": ["suspicious", "reason"]
    }
  }
                                """)
          println("🦜 : verifying array json")
          val r = jsk.validate(oJson)
          if (r.isNotEmpty()){
              println("🦜 : error checking schema")
              var s = ""
              for (e in r){
                  s += e.message
              }
              throw RuntimeException(s)
          }
          // println("\t ✅️ : schema check passed, jsk: ${S.GREEN}$jsk${S.NOR}, array: $oJson")
          println("\t ✅️ : schema check passed: ${S.GREEN}$oJson${S.NOR}")
          return oJson
      }
    fun abort(){
        running.set(false)
        D.set(-1L)
    }

    fun done(): Boolean{
        return D.get() == N.get()
    }

    fun perc(): Int {
        if (N.get() == 0L) return 0
        return (D.get() * 100 / N.get()).toInt()
    }

    fun start(repo: OneoffProcessInfo2Repo,
              type: AuditType,
              url0: String?,
              gk: String? = "",
              Nb: Int = 4): String{
        // Nb: number of batch
        this.url = if(url0.isNullOrEmpty()) "localhost:11434" else url0
        this.gk = if (gk.isNullOrEmpty()) "" else gk

        println("🦜 Audit started using url: ${ep}")


        // 0. if the work has started, then ignore it
        if (thread != null && thread!!.isAlive) return "🦜 : the audit is already running"

        // 0. start a new thread that process it
        thread = Thread(
            Runnable {
                doIt(repo, type, Nb)
            }
        )
        thread!!.start()
        return "🦜 : started the audit"
    }
}


interface FileIORecord2Repo : JpaRepository<Dt.FileIORecord2, Long>, JpaSpecificationExecutor<Dt.FileIORecord2>{
    fun findByEventTypeAndFileTo(eventType: String, fileTo: String): List<Dt.FileIORecord2>
    fun findByTimeAndEventType(time: Date, eventType: String): List<Dt.FileIORecord2>
}


@Tag(name = "Captured FIle IO Record", description = "🦜: Use these apis to get the File system IO records")
@RestController
@RequestMapping("/fs")
internal class FsController(val up : AtomicBoolean = AtomicBoolean(true)): ControllerBase(){
    @Autowired lateinit var repo: FileIORecord2Repo

    fun compile(){
        /* 🦜 : In this function, we sort things up. In particular, we
           0. get all the RENAME records whose fileTo is empty.

           1. For each of them, if there's a CREATE records at the same time,(same unix milli)
           then we combine these two into a RENAME(from,to) record.

           2. Otherwise, we turn the RENAME into a TRASH record.
         */

        // 0.
        // log.info("🦜 : starting to compile the FileIO records")
        var l = repo.findByEventTypeAndFileTo("RENAME", "")
        // log.info("🦜 : found {}{}{} records to compile", S.GREEN, l.size, S.NOR)

        val toBeDeleted = mutableListOf<Dt.FileIORecord2>()
        // 1.
        for (r in l){
            var l1 = repo.findByTimeAndEventType(r.time, "CREATE")
            // log.info("🦜 : found {}{}{} CREATE records at the same time", S.GREEN, l1.size, S.NOR)
            if (l1.size >= 1){
                var r1 = l1.first()
                r.eventType = "RENAME"
                r.fileTo = r1.fileName
                repo.save(r)
                toBeDeleted.add(r1)
            }else{
                r.eventType = "TRASH"
                repo.save(r)
            }
        }
        // log.info("🦜 : {}{}{} records to be deleted", S.GREEN, toBeDeleted.size, S.NOR)
        repo.deleteAll(toBeDeleted)
    }

    // 🦜: the following two function is copied exactly from CmdController.
    // Remember "the goal of components is for management, not code reuse"
    @GetMapping("/isUp") @Operation(summary = "Whether the streaming of FileIO is on")
    fun isUp(): W<Boolean> = W(0, up.get())
    @GetMapping("/toggle") @Operation(summary = "Toggle the status of FileIO streaming")
    fun toggle(): W<Boolean> {
        up.set(!up.get())
        if (up.get()) {
            startStreaming()
        } else {
            // log.info("🐢 : oneoff-cmd tapping paused")
        }
        return W(0, up.get())
    }

    fun startStreaming(){
        /*
         🦜 : This is pretty-much copied from the Cmd Controller 
         */
        val run = Thread (
            Runnable {
                log.info("🐢 : starting to get FileIO records from agent")
                runBlocking {
                    try {
                        val r = c.stub.streamFileIO(pb_helloRequest({})) //🦜 :here we can use a name='' to overwrite the path
                        r.collect {
                            if (!up.get()) cancel()
                            val p = Dt.FileIORecord2.convertFromPb(it)
                            log.info("🐢 : got FileIO record from agent: {}", p)
                            repo.save(p)
                        }
                    } catch (e: StatusException) {
                        log.error("🐢 : {}Error getting FileRecordsIO: {}{}, 👋 bye bye, not plan to try again", S.RED, e.message, S.NOR)
                    }
                }
            }
        )
        run.start()
        log.info("🐢 : started the FileIoRecords thread")
    }

    @EventListener
    fun init(e: ApplicationReadyEvent?) {
        // 1. start streaming
        startStreaming()
    }
    
    @GetMapping("/get") @Operation(summary = "获取文件操作记录")
    fun getFs(
        @RequestParam(defaultValue = "1") @Parameter(
            description = "page number（1-based）",
            examples = [ExampleObject(name = "1", value = "1", description = "The first page")]
        ) pageNum: Int,
        @RequestParam(defaultValue = "2") @Parameter(
            description = "how many records per page",
            examples = [ExampleObject(name = "2", value = "2", description = "2 records per page")]
        ) pageSize: Int,
        @RequestParam(required = false) @Parameter(
            description = "file name",
            examples = [ExampleObject(name = "hi.txt", value = "hi.txt", description = "Search the file hi.txt")]
        ) fileName: String?,
        @RequestParam(required = false) @Parameter(
            description = "Event type",
            examples = [ExampleObject(name = "CREATE", value = "CREATE", description = "Search the CREATE event")]
        ) eventType: String?,
        @RequestParam(required = false) @Parameter(
            description = "User Name",
            examples = [ExampleObject(name = "me", value = "me", description = "Search the result for me")]
        ) user: String?,
        @RequestParam(required = false) @Parameter(
            description = "hash",
            examples = [ExampleObject(name = "123", value = "123", description = "Search the hash 123")]
        ) hash: String?,
        @RequestParam(required = false) @Parameter(
            description = "After which time",
            examples = [ExampleObject(name = "2021-01-01 00:00:00", value = "2021-01-01 00:00:00", description = "After this time")]
        ) sinceTime: String?,
        @RequestParam(required = false) @Parameter(
            description = "Before which time",
            examples = [ExampleObject(name = "2021-01-01 00:00:00", value = "2025-01-01 00:00:00", description = "Before this time")]
        ) untilTime: String?,
        @RequestParam(required = false) @Parameter(
            description = "Sort by which column", required = false,
            examples = [ExampleObject(name = "time", value = "time", description = "Sort by time"),
                        ExampleObject(name = "", value = "", description = "Sort by id")]
        ) sortBy: String?,
        @RequestParam(required = false) @Parameter(description = "Ascending", required = false) asc: Boolean?
    ): W<Dt.Page<Dt.FileIORecord2?>> {
        log.info(
            "{}🐸 : get (file io) called with pageNum={}, pageSize={}, fileName={}, eventType={}, sinceTime={}, untilTime={}, sortBy={}, asc={} {}",
            S.CYAN, pageNum, pageSize, fileName, eventType, sinceTime, untilTime, sortBy, asc, S.NOR
        )
        // 0. compile
        compile()

        // 1. prepare pageable
        val sortBys: Set<String> = setOf("time","id")
        val pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "id")
        val page: Page<Dt.FileIORecord2?>

        // 2. prepare spec
        var spec = Specification.where<Dt.FileIORecord2?>(null)
        if (fileName != null && fileName.isNotEmpty()) {
            spec = spec.and(Dt.FileIORecord2_.fileNameContains(fileName))
        }

        if (eventType != null && eventType.isNotEmpty()) {
            spec = spec.and(Dt.FileIORecord2_.eventTypeContains(eventType))
        }
        if (user != null && user.isNotEmpty()) {
            spec = spec.and(Dt.FileIORecord2_.userContains(user))
        }
        if (hash != null && hash.isNotEmpty()) {
            spec = spec.and(Dt.FileIORecord2_.hashContains(hash))
        }

        if (sinceTime != null && sinceTime.isNotEmpty()) {
            val sinceTimeLocalDateTime = LocalDateTime.parse(sinceTime, fmt)
            val sinceTimeDate = new2Old(sinceTimeLocalDateTime)
            spec = spec.and(Dt.FileIORecord2_.timeAfter(sinceTimeDate))
        }

        if (!untilTime.isNullOrEmpty()) {
            val untilTimeLocalDateTime = LocalDateTime.parse(untilTime, fmt)
            val untilTimeDate = new2Old(untilTimeLocalDateTime)
            spec = spec.and(Dt.FileIORecord2_.timeBefore(untilTimeDate))
        }

        // 3. find and return
        page = repo.findAll(spec, pageable)
        return W(0, Dt.Page(page.content, page.totalElements, page.totalPages))
    }

}


data class OneoffPair(val uid: Int, val argv: String) //🦜: copilot says we need this to catch the result
interface OneoffProcessInfo2Repo : JpaRepository<Dt.OneoffProcessInfo2, Long>,
                                   JpaSpecificationExecutor<Dt.OneoffProcessInfo2> {

    @Query("select distinct new aaa.OneoffPair(p.uid, p.argv) from OneoffProcessInfo2 p where p.hash = ''")
    fun findDistinctPairs(): List<OneoffPair>

    @Query("select p from OneoffProcessInfo2 p where p.uid = :uid and p.argv = :argv and p.hash = '' order by p.id desc")
    fun findByUidAndArgv(uid: Int, argv: String): List<Dt.OneoffProcessInfo2>

    fun findAllByHash(hash: String): List<Dt.OneoffProcessInfo2>

    fun countBySpsReason(r: String): Long
}



@Tag(name = "Captured Oneoff Command Record", description = "🦜: Use these apis to get the oneoff process info")
@RestController
@RequestMapping("/cmd")
internal class CmdController(val up: AtomicBoolean = AtomicBoolean(true),
                             var pk: String = "") : ControllerBase() {

    @Autowired lateinit var chn: ChainController
    @Autowired lateinit var usr: UserService
    var ollamaUrl: String = "localhost:11434"
    var oven0: AiAuditOven? = null

    val oven: AiAuditOven get(){
            if (oven0 == null){
                log.info("🦜 : oven0 is null, creating a new one with url={}", ollamaUrl)
                usr.getUsers()
                oven0 = AiAuditOven(usr.cachedUsers, ollamaUrl)
            }
            return oven0!!
        }

    @GetMapping("/startAudit") @Operation(summary = "Start the audit process")
    fun startAudit(
        @RequestParam(required = false) @Parameter(
            description = "the audition type", required = false,
            examples = [ExampleObject(name = "mock", value = "mock", description = "the dummy audition"),
                        ExampleObject(name = "ollama", value = "ollama", description = "use ollama as audition type")]
        ) type: String?,
        @RequestParam(required = false) @Parameter(
            description = "overwrite the ollama url", required = false,
            examples = [ExampleObject(name = "localhost:11434", value = "localhost::11414", description = "the ollama url")]
        ) url: String?,
        @RequestParam(required = false) @Parameter(
            description = "overwrite the gemini key", required = false,
            examples = [ExampleObject(name = "", value = "", description = "no key used")]
        ) gk: String?,

        @RequestParam(required = false) @Parameter(
            description = "N batch", required = false,
            examples = [ExampleObject(name = "4", value = "4", description = "4 records per batch")]
        ) nb: Int?
    ): W<String>{

        var t: AuditType = AuditType.MOCK
        if (!type.isNullOrEmpty()){
            t = when(type.lowercase(Locale.getDefault())){
                "mock" -> AuditType.MOCK
                "ollama" -> AuditType.OLLAMA
                "gemini" -> AuditType.GEMINI
                "qwen25" -> AuditType.QWEN25
                else -> throw RuntimeException("🐢 : unknown audit type: $type")
            }
        }


        return W(0,oven.start(repo,t,url, gk, nb ?: 4))
    }

    @GetMapping("/percAudit") @Operation(summary = "Check the audit process by percentage")
    fun percAudit(): W<Int> = W(0, oven.perc())
    @GetMapping("/abortAudit") @Operation(summary = "Abort the audit process")
    fun abortAudit(): W<String> {
        oven.abort()
        return W(0, "🦜 : aborted the audit")
    }

    companion object {
        fun sha256ByteArray(data: ByteArray): String {
            val digest = MessageDigest.getInstance("SHA-256")
            val hash = digest.digest(data)
            return hash.joinToString("") { "%02x".format(it) }
        }

        fun readOutput(inputStream: InputStream): List<String> {
            BufferedReader(InputStreamReader(inputStream)).use { output ->
                return output.lines().collect(Collectors.toList())
            }
        }
    }

    // <2024-11-19 Tue>
    fun fold(): Int {
        // 1. make sure that we are not streaming
        if (up.get()) throw RuntimeException("🐢 : Can't compile/fold when streaming")

        // 2.get all unique pairs in repo whose hash is not empty, if there's
        // none, bye
        val pairs = repo.findDistinctPairs()
        log.info("🦜: found {}{}{} pairs", S.GREEN, pairs.size, S.NOR)
        if (pairs.isEmpty()){
            log.info("🐢 : no foldabe pairs found, bye 👋")
            return 0
        }

        // 3. compile
        // 3.1 for each pairs, get the records whose hash is empty
        var n = 0               //total number of records folded
        for (pi in pairs){
            var p = repo.findByUidAndArgv(pi.uid, pi.argv)
            if (p.size > 2){
                // fold the intermediate results
                // for (i in 1..p.size-2){ // if p.size = 4, do 1,2
                //     repo.delete(p[i])
                //     p[0].nFolded++
                //     n++
                // }
                // --------------------------------------------------
                // <2024-11-26 Tue> 🦜: u know what? let's do an optimization:
                val nf = p.size - 2
                p[0].nFolded += nf
                n += nf
                repo.deleteAll(p.subList(1, p.size-1))

            }
            // update the folded record 0
            repo.save(p[0])
        }
        log.info("🦜 : folded {}{}{} records", S.GREEN, n, S.NOR)
        return n
    }
    @Autowired
    val json = ObjectMapper()

    data class RegisterGroup(var rs: List<Dt.OneoffProcessInfo2>, var packHash: String, var txHash: String)

    fun pkLines(): List<String> = pk.trim().split("\n")

    fun makeFileName(i: Int): String {
        // <pk[:10]>-YYYY-MM-DD-HH:mm-<idx:00>.bin
        val df = SimpleDateFormat("yyyy-MM-dd-HH:mm")
        val date = df.format(Date())

        val l = pkLines()
        if (l.size != 3) throw RuntimeException("pk must have 3 lines, but got ${l.size}")
        val pk1 = l[1].substring(0, 10)

        return "$pk1-$date-" + String.format("%02d", i) + ".bin"
    }

    fun regStep41(s: Int = 20): MutableMap<String, RegisterGroup>{
        // s: chunk size

        // register step 4.1: prepare the register groups
        // 1. get all the records whose hash is empty
        val rs = repo.findAllByHash("")

        // 2. chunk them into groups of 20
        val rs1 = rs.chunked(s)

        val rgs = mutableMapOf<String, RegisterGroup>()
        // 3. make the register groups
        for (i in 0 until rs1.size){
            val rg = RegisterGroup(rs1[i], "", "")
            var hu = "zero://" + makeFileName(i)
            // 🦜 : what does it mean by zero://? 🐢 : It's just a fancy way
            // saying that this file is gonning to be stored in the managed
            // ./zero/ folder...
            rgs[hu] = rg

            // 4. we can also set the hashUri here, also the registerTime
            for (r in rg.rs){
                r.hashUri = hu
                r.registerTime = Date()
            }
        }
        return rgs
    }

    fun regStep42(m: MutableMap<String, RegisterGroup>): MutableMap<String, RegisterGroup>{
        // 🦜 : In this step we actually persist the RegisterGroup and calculate
        // the packHash for each file

        // message OneoffPack {
        //     repeated OneoffProcessInfo pack = 1;
        //     string pk = 2;
        //     int64  unixMilli = 3;
        // }
        for ((k,v) in m){
            val l = v.rs.map { it.toPb() }
            var pak = pb_oneoffPack{
                pack.addAll(l)
                pk = pk
                unixMilli = Instant.now().toEpochMilli()
            }

            // persist it according to k
            if (k.startsWith("zero://")){
                // 1. save the pack to a file
                val f = File("desktop/zero/" + k.substring(7))
                val ba = pak.toByteArray()
                f.writeBytes(ba)

                // 2. calculate the hash
                v.packHash = sha256ByteArray(ba)
            }else{
                throw RuntimeException("🐢 : unknown scheme: $k")
            }
        }
        return m
    }


    fun regStep43(m: MutableMap<String, RegisterGroup>): MutableMap<String, RegisterGroup>{
        // 🦜 : In this step, we prepare the txs, sign the txs and send the txs,
        // these requires: 1. the ./desktop/wch is working, 2, the
        // ChainController can talk to an endpoint

        // 1. prepare the txs
        val l = mutableListOf<Map<String, String>>()
        for ((k, v) in m){
            val o = mapOf("uri" to k, "hash" to v.packHash)
            l.add(o)
        }

        // 5.2 wrap the string into the 
        // {"type" : "data", "to" : "", "data" : "<json-string>", "nonce" : 123}
        val txs = l.mapIndexed { i, s ->
            mapOf("type" to "data", "to" to "",
                          "data" to json.writeValueAsString(s), "nonce" to i)
        }

        // 5.3 save the txs json (array)
        val pr = "./desktop/"
        File(pr + "txs-pre.json").writeText(json.writeValueAsString(txs))

        // 6. invoke the chain
        val ps = ProcessBuilder(pr + "wch", "toolbox", "tx-sign-no-crt",
                                pr + "txs-pre.json",
                                pr + "sk.pem",
                                pr + "txs-post.json")
        val p = ps.start()
        p.waitFor()
        val output = readOutput(p.inputStream)
        val error = readOutput(p.errorStream)
        log.info("⚙️ output of wch signing: {}", output)
        log.info("⚙️ error of wch signing: {}", error) // 21

        // 6.1 now we can read the signed txs
        val txsPost = File(pr + "txs-post.json").readText()
        log.info("⚙️ : read: txs-post: {}{}{}", S.GREEN, txsPost, S.NOR)

        // 6.2 and send that to the chain: curl --data @txs-post http://localhost:7777/add_txs
        var r = chn.post("add_txs",txsPost)
        log.info("response from chain: {}{}{}", S.BLUE, r.body, S.NOR)

        // 7. the returned values contain the txHashes
        // [{"hash": "hash1"}, {"hash": "hash2"}, {"hash": "hash3"]
        // 7.1 reserialize them into string
        val a = json.readTree(r.body) as ArrayNode

        if (a.size() != l.size) throw RuntimeException("🐢 : the number of txs returned is not the same as the number of txs sent")

        // 7.2 now we can fill in the txHash field of m
        for (i in l.indices) {
            var o = l[i]
            val rg = m[o["uri"]]
            if (rg == null) throw RuntimeException("🐢 : can't find the RegisterGroup for uri: ${o["uri"]}")

            rg.txHash = a[i]["hash"].toString()
            for (x in rg.rs){  //register group
                x.hash = rg.txHash
            }
        }

        return m
    }

    fun regStep44(m: MutableMap<String, RegisterGroup>): Int{
        // 🦜 : In this step, we save the records back to the database
        var n = 0
        for ((k,v) in m){
            for (r in v.rs){
                repo.save(r)
                n++
            }
        }
        return n
    }

    @GetMapping("/fold") @Operation(summary = "Fold the collected 1offCmds")
    fun foldCmds(): W<Int> = W(0, fold())

    @GetMapping("/reg") @Operation(summary = "Register the records on the chain")
    fun regCmds(): W<Int> {
        var m = regStep41()
        m = regStep42(m)
        m = regStep43(m)
        val n = regStep44(m)    //number of records saved
        return W(0, n)
    }


    @Hidden
    @GetMapping("/throw")
    fun throwException(): String {
        throw RuntimeException("just a test for the global exception handler")
    }

    @Autowired lateinit var repo: OneoffProcessInfo2Repo

    @GetMapping("/pk") @Operation(summary = "Get the public key for transaction sending")
    fun getPk(): W<String> = W(0, pk)

    @GetMapping("/isUp") @Operation(summary = "Whether the streaming of 1offInfo is on")
    fun isUp(): W<Boolean> = W(0, up.get())

    @GetMapping("/toggle") @Operation(summary = "Toggle the status of 1offInfo streaming")
    fun toggle(): W<Boolean> {
        up.set(!up.get())
        if (up.get()) {
            // log.info("🐢 : oneoff-cmd tapping resumed")
            startStreaming()
        } else {
            // log.info("🐢 : oneoff-cmd tapping paused")
        }
        return W(0, up.get())
    }

    @GetMapping("/get")
    @Operation(summary = "获取用户操作记录")
    fun getCmds(
        @RequestParam(defaultValue = "1") @Parameter(
            description = "页码（从1开始）",
            examples = [ExampleObject(name = "1", value = "1", description = "第一页")]
        ) pageNum: Int,
        @RequestParam(defaultValue = "2") @Parameter(
            description = "每页多少条",
            examples = [ExampleObject(name = "2", value = "2", description = "每页2条")]
        ) pageSize: Int,
        @RequestParam(required = false) @Parameter(
            description = "uid 用户id",
            examples = [ExampleObject(name = "0", value = "0", description = "查找用户(uid==1)的记录"),
                        ExampleObject(name = "", value = "", description = "查找所有用户的记录")]
        ) uid: Int?,
        @RequestParam(required = false) @Parameter(
            description = "某个命令关键字", required = false,
            examples = [ExampleObject(name = "sh", value = "sh", description = "查找包含sh的命令")]
        ) cmd: String?,
        @RequestParam(required = false) @Parameter(
            description = "某个时间以后登录的", required = false,
            examples = [ExampleObject(name = "2021-01-01 00:00:00", value = "2021-01-01 00:00:00", description = "某个时间后登录的")]
        ) sinceTime: String?,
        @RequestParam(required = false) @Parameter(
            description = "某个时间以前登录的", required = false,
            examples = [ExampleObject(name = "2021-01-01 00:00:00", value = "2021-01-01 00:00:00", description = "某个时间前登录的")]
        ) untilTime: String?,
        @RequestParam(required = false) @Parameter(
            description = "根据存证哈希查询", required = false,
            examples = [ExampleObject(name = "aabbcc", value = "aabbcc", description = "查询存证哈希中包含aabbcc的记录")]
        ) hash: String?,
        @RequestParam(required = false) @Parameter(
            description = "按哪行排序", required = false,
            examples = [ExampleObject(name = "time", value = "time", description = "按发现时间排序"),
                        ExampleObject(name = "", value = "", description = "默认按id排序")]
        ) sortBy: String?,
        @RequestParam(required = false) @Parameter(description = "是否升序", required = false) asc: Boolean?
    ): W<Dt.Page<Dt.OneoffProcessInfo2?>> {
        log.info(
            "{}🐸 : get (oneoff) called with pageNum={}, pageSize={}, uid={}, sinceTime={}, untilTime={}, sortBy={}, asc={} {}",
            S.CYAN, pageNum, pageSize, uid, sinceTime, untilTime, sortBy, asc, S.NOR
        )

        // 1. prepare pageable
        val sortBys: Set<String> = setOf("time","id","spsScore")
        val pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "id")
        val page: Page<Dt.OneoffProcessInfo2?>

        // 2. prepare spec
        var spec = Specification.where<Dt.OneoffProcessInfo2?>(null)
        if (uid != null) {
            if (uid < 0)
                throw IllegalArgumentException("uid must be positive")
            spec = spec.and(Dt.OneoffProcessInfo2_.uidEquals(uid))
        }

        if (cmd != null && cmd.isNotEmpty()) {
            spec = spec.and(Dt.OneoffProcessInfo2_.cmdContains(cmd))
        }

        if (sinceTime != null && sinceTime.isNotEmpty()) {
            val sinceTimeLocalDateTime = LocalDateTime.parse(sinceTime, fmt)
            val sinceTimeDate = new2Old(sinceTimeLocalDateTime)
            spec = spec.and(Dt.OneoffProcessInfo2_.timeAfter(sinceTimeDate))
        }

        if (!untilTime.isNullOrEmpty()) {
            val untilTimeLocalDateTime = LocalDateTime.parse(untilTime, fmt)
            val untilTimeDate = new2Old(untilTimeLocalDateTime)
            spec = spec.and(Dt.OneoffProcessInfo2_.timeBefore(untilTimeDate))
        }

        if (hash != null && hash.isNotEmpty()) {
            spec = spec.and(Dt.OneoffProcessInfo2_.hashContains(hash))
        }

        // 3. find and return
        page = repo.findAll(spec, pageable)
        return W(0, Dt.Page(page.content, page.totalElements, page.totalPages))

    }

    fun startStreaming(){
        /* <2024-03-02 Sat> 🦜: start a thread to fetch OneoffProcessInfo from
           the agent

           rpc StreamOneoffProcessInfo (Empty) returns (stream OneoffProcessInfo) {}
         */
        val run = Thread (
            Runnable {
                log.info("🐢 : starting to get OneoffProcessInfo from agent")
                runBlocking {
                    try {
                        val r = c.stub.streamOneoffProcessInfo(pb_empty({}))
                        r.collect {
                            if (!up.get()) cancel()
                            val p = Dt.OneoffProcessInfo2.convertFromPb(it)
                            log.info("🐢 : got OneoffProcessInfo from agent: {}", p)
                            repo.save(p)
                            // https://kotlinlang.org/docs/flow.html#flow-cancellation-checks
                            /*
                             🦜 : Now, we also throw a Failure record if the process is not in the register

                             🐢 : What about add these into a queue, and only check these when queried?

                             🦜 : That's a good, then we need a fifo queue right?

                             🐢 : Yeah, just a concurrent array will do. Here we use an java.util.ConcurrentLinkedQueue
                             */
                            //toBeProcessedOneoffProcessInfo.add(p)
                        }
                    } catch (e: StatusException) {
                        log.error("🐢 : {}Error getting OneoffProcessInfo: {}{}, 👋 bye bye, not plan to try again", S.RED, e.message, S.NOR)
                    }
                }
            }
        )
        run.start()
        log.info("🐢 : started the OneoffProcessInfo thread")
    }


    @EventListener
    fun init(e: ApplicationReadyEvent?) {
        // 1. start streaming
        startStreaming()
        // 2. get the pk if exits
        val f = File("desktop/pk.pem")
        if (f.exists()) pk = f.readText()
    }

    //val toBeProcessedOneoffProcessInfo: ConcurrentLinkedQueue<Dt.OneoffProcessInfo2> = ConcurrentLinkedQueue()


} // class CmdHistoryController

@Tag(name = "Chain related API", description = "APIs related to chain")
@RestController
@RequestMapping("/chain")
internal class ChainController(var url: String = "http://localhost:7777"): ControllerBase(){

    @Autowired
    lateinit var json: ObjectMapper

    fun ping(): Boolean {
        // ping the url
        val r: ResponseEntity<String> = request("/get_node_status")
        // 🦜 : here let's just randomly ping an API, in practice, we don't need
        // to ping this... The smart spring should figure this out and throw the
        // proper exceptions for us....So yeah, this API is for testing only
        return r.statusCode.is2xxSuccessful()
    }

    // post
    fun post(path: String, body: String): ResponseEntity<String> {
        // val restTemplate = RestTemplate()
        // val headers = HttpHeaders()
        // headers.set("Content-Type", "application/json")
        // val entity = HttpEntity(body, headers)
        // return restTemplate.exchange(url + path, HttpMethod.POST, entity, String::class.java)

        // --------------------------------------------------
        val client = WebClient.create(url)
        val r = client.post()
            .uri(path)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(body)
            .retrieve()
            .toEntity(String::class.java)
            .block()
        return r!!

    }

    @GetMapping("/url")
    fun url1(): W<String> {
        return W(0, url)
    }

    @Operation(summary = "Get node status",
               responses = [
                   ApiResponse(
                       description = "Successful response",
                       content = [
                           Content(
                               mediaType = "application/json",
                               schema = Schema(implementation = JsonNode::class),
                               examples = [ExampleObject(
                                               // value = "{\"status\":\"ok\"}"
                                               value = """
{
  "code": 0,
  "data": {"cnsss": "listenToOne", "is_primary": true,
    "listened_endpoint": {"pk": "<mock-pk>", "addr_port": "localhost:7777", "cert": ""},
    "known_subs": [],
    "txs_count": 1,
    "latest_blk": {
      "number": 0,
      "hash": "3cb681d659097c833465914c3591ee770994de0ca66a038facd538b8c0e4c354",
      "parentHash": "0000000000000000000000000000000000000000000000000000000000000000",
      "txs": [
        {
          "from": "0000000000000000000000000000000000000001",
          "to": "0000000000000000000000000000000000000000",
          "nonce": 123,
          "timestamp": 1731637422,
          "hash": "b06b89b665df6f7ff1be967faf9a0601f71c0a3cdb8250c48fe7fcc663b18d1b",
          "type": "data",
          "data": "This is the initial block"
        }
      ],
      "stateChanges": [[]],
      "txReceipts": [{"ok": true, "result": "", "type": "data"}]
    }
  },
  "msg": "success"
}
                                                   """
                                           )]
                           )
                       ]
                   )
               ]
    )
    @GetMapping("/getNodeStatus")
    fun nodeStatus1(
        @RequestParam(required = false) @Parameter(
            description = "set the chain url before calling this API, if not set, use the default node url = `http://localhost:7777` will be used. (remember to set it back)",
            examples = [
                ExampleObject(name = "", value =  "", description = "use the default node url"),
                ExampleObject(name = "http://localhost:7777", value =  "http://localhost:7777", description = "still the default node url")
            ]
        ) url: String?,
    ): W<JsonNode> {
        if (url != null && url.isNotEmpty()) {
            this.url = url
        }
        return W(0, nodeStatus())
    }

    fun nodeStatus(): JsonNode{
        /*
         🦜 : Okay, here's what we gonna do:
         1. call /get_cnsss_info -> o
         2. o["txs_count"] = (/get_node_status)["txs_count"]
         3. o["latest_blk"] = (/get_latest_Blk)[0]
         */
        val o0 = getJson("/get_cnsss_info")
        val o1 = getJson("/get_node_status")
        val o2 = getJson("/get_latest_Blk")

        var o = o0 as ObjectNode
        o.replace("txs_count", o1["txs_count"])
        o.replace("latest_blk", o2[0])
        return o
    }

    // request
    fun request(path: String): ResponseEntity<String> {
        // val restTemplate = RestTemplate()
        // return restTemplate.getForEntity(url + path, String::class.java)

        // --------------------------------------------------
        val client = WebClient.create(url)
        val r = client.get()
            .uri(path)
            .retrieve()
            .toEntity(String::class.java)
            .block()
        return r!!
    }

    @Throws(JsonProcessingException::class)
    fun getJson(path: String): JsonNode {
        val r: ResponseEntity<String> = request(path)
        if (r.getStatusCode().is2xxSuccessful()) {
            return json.readTree(r.getBody())
        } else {
            log.error("failed to get json: {}", r)
            throw RuntimeException("failed to get json")
        }
    }
}

class W<T>(var code: Int, var data: T, var msg: String = "success")
/**
 * @brief The base class for all controllers
 */
internal open class ControllerBase{

    @Autowired
    lateinit var c : Common //🦜 : Because we need a field.. we use <- 

    // 🦜 : Since spring 4.3, @Autowired is not needed
    // @Autowired
    // var c: Common? = null

    var fmt: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")


    companion object {
        var log: Logger = LoggerFactory.getLogger(ControllerBase::class.java)
        fun old2New(d: Date): LocalDateTime {
            // java 9
            return LocalDateTime.ofInstant(d.toInstant(), ZoneId.systemDefault())
        }

        fun new2Old(l: LocalDateTime): Date {
            return Date.from(l.atZone(ZoneId.systemDefault()).toInstant())
        }

        fun <T, Tk, Tr : JpaRepository<T, Tk>> getPage(repo: Tr, pageNum: Int, pageSize: Int): W<Dt.Page<T>?> {
            if (pageNum < 1 || pageSize < 0) {
                return W(0, null, "不合法的页码或页大小")
            }
            //从数据库中查询
            // --------------------------------------------------
            val page = repo.findAll(PageRequest.of(pageNum - 1, pageSize))
            // return new Dt.Page<T>(page.getContent(),page.getTotalElements(), page.getTotalPages());
            return W(0, Dt.Page(page.content, page.totalElements,page.totalPages)) //🦜 : the kotlin magic
        }

        fun checkThingsAndPreparePageable(
            pageNum: Int, pageSize: Int, sortBys: Set<String?>, sortBy0: String?, asc: Boolean?,
            defaultSortBy: String?
        ): Pageable {
            var sortBy = sortBy0 //change to var
            require(!(pageNum < 1 || pageSize < 0)) { "pageNum or pageSize is illegal" }
            // throws IllegalArgumentException

            if (sortBy == null || sortBy.isEmpty()) {
                sortBy = defaultSortBy
            } else require(sortBys.contains(sortBy)) { "sortBy is illegal, it can only be one of $sortBys" }

            // --------------------------------------------------
            // 🦜 : use the powerfull spring-data, so powerful
            var sort = Sort.by(sortBy)
            sort = if (asc != null && !asc) {
                sort.descending()
            } else {
                sort.ascending()
            }

            val pageable: Pageable = PageRequest.of(pageNum - 1, pageSize, sort)
            return pageable
        }
    }
}                               // ControllerBase

internal abstract class ControllerThatSyncs : ControllerBase() {
    var lastSyncTime: LocalDateTime = LocalDateTime.now().minusDays(1) // init to a time in the past
    var cacheValidDuration: Duration = Duration.ofMinutes(20)

    abstract fun sync()
    fun syncIfTooOld() {
        if (lastSyncTime.plus(cacheValidDuration).isBefore(LocalDateTime.now())) {
            log.info("🐢 : cache is too old, syncing")
            this.sync()
            lastSyncTime = LocalDateTime.now()
        }
    }
}



internal interface LoginInfoRepo : JpaRepository<Dt.LoginInfo2, Long>, JpaSpecificationExecutor<Dt.LoginInfo2> {
    fun countByLoginTimeBetween(since: Date, until: Date): Long
}


@Tag(name = "基础管理", description = "🦜: 基础系统管理功能，包括查看用户,登入登出时间, 电脑配置等。")
@RestController
@RequestMapping("/basic")
internal class BasicController : ControllerThatSyncs() {

    @Hidden                     // 🦜 : Yeah, hidden in swagger
    @GetMapping("/bye")
    fun bye(): W<String> {

        // schedule to shut down in 1s (in a new thread)
        val run = Thread(
            Runnable {
                log.info("🦜 : shutting down")
                Thread.sleep(1000)
                System.exit(0)
            }
        )
        run.start()

        /* 🦜 : Nope, scheduler is not the right fit here...Neither is the
           kotlin coroutine. */

        return W(0, "shutdown scheduled in 1s")
    }
    // 🦜 : curl localhost:8081/basic/bye

    @Autowired
    lateinit var loginInfoRepo: LoginInfoRepo

    @Autowired lateinit var userService: UserService

    @Operation(summary = "获取用户列表")
    @GetMapping("/getUsers")
    fun getUsers(): W<List<Dt.UserInfo2>>  = userService.getUsers()

    fun askLoginInfoFromAgent(sinceTime0: LocalDateTime, untilTime0: LocalDateTime, user0: String?): List<Dt.LoginInfo2> {

        // 1. prepare the request
        // val bl = GetLoginHistoryReq.newBuilder()
        //     .setSinceTime(sinceTime.format(fmt))
        //     .setUntilTime(untilTime.format(fmt))
        val b = pb_getLoginHistoryReq {
            sinceTime = sinceTime0.format(fmt)
            untilTime = untilTime0.format(fmt)
            user = user0 ?: ""
        }

        // 2. call the agent
        val r: pb_GetLoginHistoryRpl
        try {
            runBlocking {
                r = c.stub.getLoginHistory(b)
            }
        } catch (e: StatusException) {
            val msg = e.status.description
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.status)
            throw RuntimeException(msg)
        }

        // 3. pb -> java
        val l: MutableList<Dt.LoginInfo2> = ArrayList()
        for (i in r.loginInfosList) {
            l.add(Dt.LoginInfo2.convertFromPb(i))
        }

        // 4. return
        return l
    }


    // 🦜 : First let's override the `sync()` method
    override fun sync() {
        // 1. call the agent to get the list
        // --------------------------------------------------
        val untilTime = LocalDateTime.now()
        // a year ago
        val sinceTime = untilTime.minusYears(1)
        val l = this.askLoginInfoFromAgent(sinceTime, untilTime, "")

        // ^^^ get all the login history in the past year

        // 2. update the repo
        // --------------------------------------------------
        loginInfoRepo.deleteAll()
        loginInfoRepo.saveAll(l)

    }


    @Autowired
    lateinit var t: ThreadPoolTaskScheduler

    @Autowired
    lateinit var json: ObjectMapper

    @GetMapping("/getLoginHistory")
    @Operation(summary = "获取用户登录历史")
    fun getLoginHistory(
        @RequestParam(defaultValue = "1") @Parameter(
            description = "页码（从1开始）",
            examples = [ExampleObject(name = "1", value = "1", description = "第一页")]
        ) pageNum: Int,
        @RequestParam(defaultValue = "2") @Parameter(
            description = "每页多少条",
            examples = [ExampleObject(name = "2", value = "2", description = "每页2条")]
        ) pageSize: Int,
        @RequestParam(required = false) @Parameter(
            description = "用户名",
            examples = [ExampleObject(name = "aaa", value = "aaa", description = "查找用户aaa的记录"), ExampleObject(
                name = "",
                value = "",
                description = "查找所有用户的记录"
            )]
        ) user: String?,
        @RequestParam(required = false) @Parameter(
            description = "某个时间以后登录的",
            required = false,
            examples = [ExampleObject(
                name = "2021-01-01 00:00:00",
                value = "2021-01-01 00:00:00",
                description = "某个时间后登录的"
            )]
        ) sinceTime: String?,
        @RequestParam(required = false) @Parameter(
            description = "某个时间以前登录的",
            required = false,
            examples = [ExampleObject(
                name = "2021-01-01 00:00:00",
                value = "2021-01-01 00:00:00",
                description = "某个时间前登录的"
            )]
        ) untilTime: String?,
        @RequestParam(required = false) @Parameter(
            description = "按哪行排序",
            required = false,
            examples = [ExampleObject(
                name = "loginTime",
                value = "loginTime",
                description = "默认按登录时间排序"
            ), ExampleObject(name = "logoutTime", value = "logoutTime", description = "按登出时间排序")]
        ) sortBy: String?,
        @RequestParam(required = false) @Parameter(description = "是否升序", required = false) asc: Boolean?
    ): W<Dt.Page<Dt.LoginInfo2?>> {
        log.info(
            "{}🐸 : getLoginHistory called with pageNum={}, pageSize={}, user={}, sinceTime={}, untilTime={}, sortBy={}, asc={} {}",
            S.CYAN, pageNum, pageSize, user, sinceTime, untilTime, sortBy, asc, S.NOR
        )

        this.syncIfTooOld()

        // 1. prepare pageable
        val sortBys: Set<String> = setOf("loginTime", "logoutTime")
        val pageable = checkThingsAndPreparePageable(pageNum, pageSize, sortBys, sortBy, asc, "loginTime")
        val page: Page<Dt.LoginInfo2?>

        // 2. prepare spec
        var spec = Specification.where<Dt.LoginInfo2?>(null)
        if (user != null && user.isNotEmpty()) {
            spec = spec.and(Dt.LoginInfo2_.userContains(user))
        }
        if (sinceTime != null && sinceTime.isNotEmpty()) {
            val sinceTimeLocalDateTime = LocalDateTime.parse(sinceTime, fmt)
            val sinceTimeDate = new2Old(sinceTimeLocalDateTime)
            spec = spec.and(Dt.LoginInfo2_.loginTimeAfter(sinceTimeDate))
        }

        if (!untilTime.isNullOrEmpty()) {
            val untilTimeLocalDateTime = LocalDateTime.parse(untilTime, fmt)
            val untilTimeDate = new2Old(untilTimeLocalDateTime)
            spec = spec.and(Dt.LoginInfo2_.loginTimeBefore(untilTimeDate))
        }

        // 3. find and return
        page = loginInfoRepo.findAll(spec, pageable)
        // log.info("{}🐸 : found page of {}", S.CYAN, page.getTotalElements() , S.NOR);
        return W(0, Dt.Page(page.content, page.totalElements, page.totalPages))
    }

    @EventListener // 🦜 : What's this ? 🐢 : This is the spring magic to run something on startup.
    fun init(e: ApplicationReadyEvent?) {
    }

} // class BasicController




@Component
internal class UserService : ControllerThatSyncs(){
    // 🦜 : Provide the users to the controllers

    fun getUsers(): W<List<Dt.UserInfo2>> {
        /*
         [2024-03-04]: Update, we cache this result.
         */
       syncIfTooOld()
        // return W(0, Dt.Page(users, r.usersCount.toLong()))
        // return W(0, Dt.Page(cachedUsers.values.toList(), cachedUsers.size.toLong()))
        return W(0, cachedUsers.values.toList())
    }

    val cachedUsers : ConcurrentHashMap<Int, Dt.UserInfo2> = ConcurrentHashMap()

    override fun sync(){
        //  get the user list
        /*
         🦜 : There won't be many, so we don't bother sql|paging here
         */
        val b = pb_empty({})
        val r: pb_GetUsersRpl
        try {
            runBlocking {
                r = c.stub.getUsers(b)
            }
        } catch (e: StatusException) {
            val msg = e.status.description
            log.error(S.RED + "📕️ RPC failed: {}" + S.NOR, e.status)
            throw RuntimeException(msg)
        }

        // pb -> java
        this.cachedUsers.clear()
        for (u in r.usersList) {
            // users.add()
            val u0 = Dt.UserInfo2.convertFromPb(u)
            this.cachedUsers[u0.uidInt] = u0
        }
    }
}

internal class DayInterval(var since: Date, var until: Date) {
    val sinceString: String
        get() = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(since)

    val untilString: String
        get() = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(until)

    companion object {
        fun nDaysFromNow(d: Int): DayInterval {
            val c = Calendar.getInstance() // now
            c.add(Calendar.DATE, -d)
            // set hh:mm:ss to 00:00:00
            c[Calendar.HOUR_OF_DAY] = 0
            c[Calendar.MINUTE] = 0
            c[Calendar.SECOND] = 0

            val since = c.time
            c.add(Calendar.DATE, 1)
            val until = c.time

            return DayInterval(since, until)
        }
    }
}



// ^^^ controllers
// --------------------------------------------------
object S {
    const val RED: String = "\u001B[31m"
    const val GREEN: String = "\u001B[32m"
    const val YELLOW: String = "\u001B[33m"
    const val BLUE: String = "\u001B[34m"
    const val MAGENTA: String = "\u001B[35m"
    const val CYAN: String = "\u001B[36m"
    const val NOR: String = "\u001B[0m"
}

// common stuff
class Common(agentUrl: String) : AutoCloseable {
    val json: ObjectMapper = mapper()
    val ch = ManagedChannelBuilder.forTarget(agentUrl).usePlaintext().build()
    val stub = GreeterCoroutineStub(ch)

    // dtor (finalizer)
    override fun close() {
        try {
            // ch.shutdownNow().awaitTermination(5, TimeUnit.SECONDS)
        } catch (e: InterruptedException) {
            log.error("❌️ : error closing grpc channel: ", e)
        }
    }

    companion object {
        var log: Logger = LoggerFactory.getLogger(Common::class.java)

        fun ok(): Dt.Error {
            // return json.writeValueAsString(Dt.Error.SUCCESS);
            return Dt.Error.SUCCESS
        }

        // fun bad(msg: String?, status: HttpStatus = HttpStatus.OK): Dt.Error {
        // 🦜 : Previously we can change the status code, but we have given that up..
        fun bad(msg: String?): Dt.Error {
            return Dt.Error(-1, msg ?: "unknown error (null msg)")
        }

        fun mapper(): ObjectMapper {
            val m = ObjectMapper()

            val df = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
            m.setDateFormat(df)
            return m
        }
    }
}

@OpenAPIDefinition(
    info = Info(
        title = "这是安全板卡管理系统的API",
        version = "2.2",
        description = "⚠️: 请注意所有时间戳的格式都是`yyyy-MM-dd hh:mm:ss`，" +
            "但是因为作者最终还是没能搞懂swagger的对象反映射魔法，所以在这个swagger自动生成的" +
            "文档里示例的时间戳格式一般会是一串像`2023-12-25T09:36:22.847Z`的东西，" +
            "请在发送和接受参数的时候使用 `yyyy-MM-dd hh:mm:ss` 格式。"
    )
)
@SpringBootApplication
class M {

    @Value("\${machine.agent-url}")
    lateinit var agentUrl: String

    @Bean
    fun common(): Common {
        return Common(agentUrl)
    }

    @Autowired
    lateinit var t: ThreadPoolTaskScheduler

    companion object {
        var log: Logger = LoggerFactory.getLogger(M::class.java)

        @JvmStatic
        fun main(args: Array<String>) {
            SpringApplication.run(M::class.java, *args)
        }
    }
}


@Configuration
@EnableWebMvc
internal class C1: WebMvcConfigurer{
    /*
     🦜 : By declaring a primary bean of ObjectMapper, we change how spring-boot parse and serialize json
     */
    @Primary
    @Bean
    fun json(): ObjectMapper = Common.mapper()

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/**")
            .addResourceLocations("classpath:/static/")
    }
    override fun addViewControllers(registry: ViewControllerRegistry) {
        registry.addRedirectViewController("/", "/index.html")
    }

    @Bean
    fun threadPoolTaskScheduler(): ThreadPoolTaskScheduler {
        var t = ThreadPoolTaskScheduler()
        t.poolSize = 5
        return t
    }

    override fun addCorsMappings(registry: CorsRegistry ){
        registry.addMapping("/**")
    }
}

/*
  🦜 : Here, the globle exception handler is defined
*/
@ControllerAdvice
internal class GlobalExceptionHandler {
    @ExceptionHandler(RuntimeException::class, IllegalArgumentException::class)
    fun handleException(e: Exception): ResponseEntity<Dt.Error> {
        // Customize your response here
        // return new ResponseEntity<>("An error occurred: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
        // println("🐢 handler called: " + e.message)
        // return Dt.Error(-1, e.message ?: "unknown error (null msg)")
        return ResponseEntity(Dt.Error(-1, e.message ?: "unknown error (null msg)"), HttpStatus.OK)
    }
}
